BSDF (Bidirectional Scattering Distribution Function)

Sources

Samples

BSDF (Bidirectional Scattering Distribution Function)

  • A material model is described mathematically by a BSDF  (Bidirectional Scattering Distribution Function), which is itself composed of two other functions:

    • BRDF  (Bidirectional Reflectance Distribution Function)

    • BTDF  (Bidirectional Transmittance Function).

  • Since we aim to model commonly encountered surfaces, our standard material model will focus on the BRDF and ignore the BTDF, or approximate it greatly.

  • Rendering Equation .

  • Rendering Equation .

    • {9:57}

      • Implementation.

  • Implementing the Rendering Equation .

  • Rendering Equation and BRDFs .

    • Great video, super important to understand rendering and Physics Based Rendering (PBR).

    • Everything is based on the abstract equation, as being the basis for Based Physics Rendering:

      • outgoing_light = emitted_light + reflected_light .

    • {10:19}

      • The 'reflected_light' equation is shown

      • $f_r(x, \omega_i, \omega_o)$ is the 'Bidirectional Reflectance Distribution Function (BRDF)'

      • $L_i$ is the 'color of light'.

      • $cos(\theta_i)$ is the representation of the 'Surface Normal'

      • ~Then an integral is used to calculate this at different angles, ~I don't know.

        • Engines don't use the integral.

    • {10:53}

      • Rendering Equation.

    • {22:58 -> end}

      • It's the most interesting part of the video, although everything is interesting.

      • Each parameter of the shader used by Disney is explained.

BRDF (Bidirectional Reflectance Distribution Function)

  • The BRDF describes the surface response of a standard material as a function made of two terms:

    • A diffuse component ($f_d$).

    • A specular component ($f_r$).

  • .

  • The complete surface response can be expressed as such:

  • .

  • This equation characterizes the surface response for incident light from a single direction. The full rendering equation would require to integrate $l$ over the entire hemisphere.

  • Energy conservation is one of the key components of a good BRDF for physically based rendering. An energy conservative BRDF states that the total amount of specular and diffuse reflectance energy is less than the total amount of incident energy. Without an energy conservative BRDF, artists must manually ensure that the light reflected off a surface is never more intense than the incident light.

General Terms

  • $v$

    • View unit vector.

  • $h$

    • Half unit vector between $l$ and $v$.

  • $l$

    • Incident light unit vector.

  • $n$

    • Normal surface unit vector.

  • $\alpha$

    • Roughness, remapped from using input perceptualRoughness .

TLDR

  • Specular Term :

    • A Cook-Torrance  specular microfacet model

    • A GGX  normal distribution function

    • A Smith-GGX  height-correlated visibility function.

    • A Schlick Fresnel  function.

  • Diffuse Term :

    • A Lambertian  diffuse model.

float D_GGX(float NoH, float a) {
    float a2 = a * a;
    float f = (NoH * a2 - NoH) * NoH + 1.0;
    return a2 / (PI * f * f);
}

vec3 F_Schlick(float u, vec3 f0) {
    return f0 + (vec3(1.0) - f0) * pow(1.0 - u, 5.0);
}

float V_SmithGGXCorrelated(float NoV, float NoL, float a) {
    float a2 = a * a;
    float GGXL = NoV * sqrt((-NoL * a2 + NoL) * NoL + a2);
    float GGXV = NoL * sqrt((-NoV * a2 + NoV) * NoV + a2);
    return 0.5 / (GGXV + GGXL);
}

float Fd_Lambert() {
    return 1.0 / PI;
}

void BRDF(...) {
    // >> Standard Model

    vec3 h = normalize(v + l);

    float NoV = abs(dot(n, v)) + 1e-5;
    float NoL = clamp(dot(n, l), 0.0, 1.0);
    float NoH = clamp(dot(n, h), 0.0, 1.0);
    float LoH = clamp(dot(l, h), 0.0, 1.0);

    // perceptually linear roughness to roughness (see parameterization)
    float roughness = perceptualRoughness * perceptualRoughness;

    float D = D_GGX(NoH, roughness);
    vec3  F = F_Schlick(LoH, f0);
    float V = V_SmithGGXCorrelated(NoV, NoL, roughness);

    // specular BRDF
    float D = distributionCloth(roughness, NoH);  // From the Cloth BRDF.
    float V = visibilityCloth(NoV, NoL);          // From the Cloth BRDF.
    vec3  F = sheenColor;                         // From the Cloth BRDF.
    vec3 Fr = (D * V) * F; 

    vec3 energyCompensation = 1.0 + f0 * (1.0 / dfg.y - 1.0);
    // Scale the specular lobe to account for multiscattering
    Fr *= pixel.energyCompensation;
    
    // Without Cloth BRDF
    // diffuse BRDF
    // Conversion of base color to diffuse:
    vec3 diffuseColor = (1.0 - metallic) * baseColor.rgb;
    vec3 Fd = diffuseColor * Fd_Lambert();  
    
    // With Cloth BRDF
    float diffuse = diffuse(roughness, NoV, NoL, LoH);
    #if defined(MATERIAL_HAS_SUBSURFACE_COLOR)
    // energy conservative wrap diffuse
    diffuse *= saturate((dot(n, light.l) + 0.5) / 2.25);
    #endif
    vec3 Fd = diffuse * pixel.diffuseColor;
    
    // <<
    
    
    
    // >> Cloth BRDF
    
    #if defined(MATERIAL_HAS_SUBSURFACE_COLOR)
    // cheap subsurface scatter
    Fd *= saturate(subsurfaceColor + NoL);
    vec3 color = Fd + Fr * NoL;
    color *= (lightIntensity * lightAttenuation) * lightColor;
    #else
    vec3 color = Fd + Fr;
    color *= (lightIntensity * lightAttenuation * NoL) * lightColor;
    #endif
        
    // <<
    
    
    
    // >> Clear Coat
    
    // remapping and linearization of clear coat roughness
    clearCoatPerceptualRoughness = clamp(clearCoatPerceptualRoughness, 0.089, 1.0);
    clearCoatRoughness = clearCoatPerceptualRoughness * clearCoatPerceptualRoughness;

    // clear coat BRDF
    float  Dc = D_GGX(clearCoatRoughness, NoH);
    float  Vc = V_Kelemen(clearCoatRoughness, LoH);
    float  Fc = F_Schlick(0.04, LoH) * clearCoat; // clear coat strength
    float Frc = (Dc * Vc) * Fc;

    // <<


    // account for energy loss in the base layer
    return color * ((Fd + Fr * (1.0 - Fc)) * (1.0 - Fc) + Frc);
}


void main() {
    // I believe this is completely geared towards Directional Lights.

    vec3 l = normalize(-lightDirection);
    float NoL = clamp(dot(n, l), 0.0, 1.0);
    
    // lightIntensity is the illuminance
    // at perpendicular incidence in lux
    float illuminance = lightIntensity * NoL;
    vec3 luminance = BSDF(v, l) * illuminance;
}

Specular BRDF

  • For the specular term, $f_r$ is a mirror BRDF that can be modeled with the Fresnel law , noted in the Cook-Torrance  approximation of the microfacet model integration:

  • .

  • This function can be simplified by introducing a Visibility Function.

  • .

  • .

Normal distribution function (Specular D)
  • Burley  observed that long-tailed normal distribution functions (NDF) are a good fit for real-world surfaces.

  • The GGX  distribution is a distribution with long-tailed falloff and short peak in the highlights, with a simple formulation suitable for real-time implementations. It is also a popular model, equivalent to the Trowbridge-Reitz  distribution, in modern physically based renderers.

  • .

  • Specular D term :

    float D_GGX(float NoH, float roughness) {
        float a = NoH * roughness;
        float k = roughness / (1.0 - NoH * NoH + a * a);
        return k * k * (1.0 / PI);
    }
    
  • Specular D term, optimized for fp16 :

    #define MEDIUMP_FLT_MAX    65504.0
    #define saturateMediump(x) min(x, MEDIUMP_FLT_MAX)
    
    float D_GGX(float roughness, float NoH, const vec3 n, const vec3 h) {
        vec3 NxH = cross(n, h);
        float a = NoH * roughness;
        float k = roughness / (dot(NxH, NxH) + a * a);
        float d = k * k * (1.0 / PI);
        return saturateMediump(d);
    }
    
  • .

Geometric Shadowing / Visibility Function (Specular G / Specular V)
  • Eric Heitz  showed in that the Smith  geometric shadowing function is the correct and exact term to use.

  • The Smith  formulation is the following:

  • .

  • Consider:

  • Specular V term :

    • The GLSL implementation of the visibility term, is a bit more expensive than we would like since it requires two sqrt  operations.

    float V_SmithGGXCorrelated(float NoV, float NoL, float roughness) {
        float a2 = roughness * roughness;
        float GGXV = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2);
        float GGXL = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2);
        return 0.5 / (GGXV + GGXL);
    }
    
  • Approximated specular V term :

    • This approximation is mathematically wrong but saves two square root operations and is good enough for real-time mobile applications

    float V_SmithGGXCorrelatedFast(float NoV, float NoL, float roughness) {
        float a = roughness;
        float GGXV = NoL * (NoV * (1.0 - a) + a);
        float GGXL = NoV * (NoL * (1.0 - a) + a);
        return 0.5 / (GGXV + GGXL);
    }
    
    • (2025-09-13) Note:

      • If roughness is 0, then the final result is 1 / (4 * NoL * NoV) .

      • I tested this, it's correct.

  • .

Fresnel (Specular F)
  • This effect models the fact that the amount of light the viewer sees reflected from a surface depends on the viewing angle and on the index of refraction (IOR) of the material.

  • .

    • When looking at the water straight down (at normal incidence) you can see through the water. However, when looking further out in the distance (at grazing angle, where perceived light rays are getting parallel to the surface), you will see the specular reflections on the water become more intense.

  • Schlick  describes an inexpensive approximation of the Fresnel  term for the Cook-Torrance  specular BRDF:

  • .

  • This Fresnel function can be seen as interpolating between the incident specular reflectance and the reflectance at grazing angles.

  • $f_0$ (Base Reflectance or Base Reflectivity) :

    • Is a constant that represents the specular reflectance at normal incidence and is achromatic for dielectrics, and chromatic for metals.

    • The actual value depends on the index of refraction of the interface.

    • If dia-electric: use base reflectivity of 0.04; else: is a metal, use albedo as base reflectivity.

    • n (Index of Refraction) (IOR) :

      • base_reflectivity of 0.04 is the same as IOR = 1.5.

      • IOR 1.5 is the default for blender.

      • .

    • Calculating $f_0$ and Remapping :

      • The Fresnel term relies on $f_0$ , the specular reflectance at normal incidence angle, and is achromatic for dielectrics.

      • Remapping :

        vec3 f0 = 0.16 * reflectance * reflectance
        
        • See the Material -> Reflectance part to understand the remapping.

      • Computing $f_0$ for dielectric and metallic materials in GLSL

      vec3 f0 = 0.16 * reflectance * reflectance * (1.0 - metallic) + baseColor * metallic;
      
  • $f_{90}$.

    • Reflectance at grazing angles.

    • Approaches 100% for smooth materials.

    • Observation of real world materials show that both dielectrics and conductors exhibit achromatic specular reflectance at grazing angles and that the Fresnel reflectance is 1.0 at 90°.

  • Specular F term :

    vec3 F_Schlick(float u, vec3 f0, float f90) {
        return f0 + (vec3(f90) - f0) * pow(1.0 - u, 5.0);
    }
    
    • Using $f_{90}$ set to 1, the Schlick approximation for the Fresnel term can be optimized for scalar operations by refactoring the code slightly.

    vec3 F_Schlick(float u, vec3 f0) {
        float f = pow(1.0 - u, 5.0);
        return f + f0 * (1.0 - f);
    }
    
    • .

    • Godot Code Snippet :

      float fresnel(float amount, vec3 normal, vec3 view)
      {
          return pow((1.0 - clamp(dot(normalize(normal), normalize(view)), 0.0, 1.0 )), amount);
      }
      
      void fragment()
      {
          vec3 base_color = vec3(0.0);
          float basic_fresnel = fresnel(3.0, NORMAL, VIEW);
          ALBEDO = base_color + basic_fresnel;
      }
      
      • Colorful Fresnel:

        • This snippet lets you colorize the fresnel by multiplying it with an RGB-value and set the intensity to either tone down the effect or, if you crank it up, make it glow. You need to enable Glow in the Environment node. (The clamp()  has been removed allowing the fresnel to go beyond 1.0). You can also make the fresnel glow by assigning it to EMISSION.

        • .

          • Not-colorful / colorful + glow.

        vec3 fresnel_glow(float amount, float intensity, vec3 color, vec3 normal, vec3 view)
        {
            return pow((1.0 - dot(normalize(normal), normalize(view))), amount) * color * intensity;
        }
        
        void fragment()
        {
            vec3 base_color = vec3(0.5, 0.2, 0.9);
            vec3 fresnel_color = vec3(0.0, 0.7, 0.9);
            vec3 fresnel = fresnel_glow(4.0, 4.5, fresnel_color, NORMAL, VIEW);
            ALBEDO = base_color + fresnel;
        }
        
Energy Compensation
  • .

  • Single Scaterring vs Multiscattering :

    • .

    • .

  • This solution is therefore not suitable for real-time rendering.

  • The idea is to add an energy compensation term as an additional BRDF lobe.

vec3 energyCompensation = 1.0 + f0 * (1.0 / dfg.y - 1.0);
// Scale the specular lobe to account for multiscattering
Fr *= pixel.energyCompensation;

Diffuse BRDF

  • The diffuse term of the BRDF:

  • .

  • Our implementation will instead use a simple Lambertian BRDF  that assumes a uniform diffuse response over the microfacets hemisphere:

  • .

  • Diffuse Lambertian BRDF :

    • In practice, the diffuse reflectance is multiplied later

    float Fd_Lambert() {
        return 1.0 / PI;
    }
    
    vec3 Fd = diffuseColor * Fd_Lambert();
    
  • However, the diffuse part would ideally be coherent with the specular term and take into account the surface roughness. Both the Disney diffuse BRDF  and Oren-Nayar  model take the roughness into account and create some retro-reflection at grazing angles. Given our constraints we decided that the extra runtime cost does not justify the slight increase in quality. This sophisticated diffuse model also renders image-based and spherical harmonics more difficult to express and implement.

  • Disney diffuse BRDF :

    • .

    • .

    float F_Schlick(float u, float f0, float f90) {
        return f0 + (f90 - f0) * pow(1.0 - u, 5.0);
    }
    
    float Fd_Burley(float NoV, float NoL, float LoH, float roughness) {
        float f90 = 0.5 + 2.0 * roughness * LoH * LoH;
        float lightScatter = F_Schlick(NoL, 1.0, f90);
        float viewScatter = F_Schlick(NoV, 1.0, f90);
        return lightScatter * viewScatter * (1.0 / PI);
    }
    
  • Lambertian diffuse BRDF vs Disney diffuse BRDF :

    • The material used is fully dialetric.

    • The surface response is very similar with both BRDFs but the Disney one exhibits some nice retro-reflections at grazing angles (look closely at the left edge of the spheres).

    • .

    • We could allow artists/developers to choose the Disney diffuse BRDF depending on the quality they desire and the performance of the target device. It is important to note however that the Disney diffuse BRDF is not energy conserving as expressed here.